export type RouterDirectionType = number;

export const RouterDirection: Record<string, RouterDirectionType> = {
  Up: 1,
  Down: -1,
  Left: -2,
  Right: 2,
};

export type RouterDirectionKey = keyof typeof RouterDirection;

export class Point {
  public x: number;
  public y: number;

  constructor();
  constructor(x?: number, y?: number);
  constructor(x?: number, y?: number) {
    this.x = x == null ? 0 : x;
    this.y = y == null ? 0 : y;
  }

  clone() {
    return Point.clone(this);
  }

  /**
   * Moves this point along the line starting from `ref` to this point by a
   * certain `distance`.
   */
  move(ref: Point.PointLike | Point.PointData, distance: number) {
    const p = Point.create(ref);
    const rad = Angle.toRad(p.theta(this));
    return this.translate(Math.cos(rad) * distance, -Math.sin(rad) * distance);
  }

  /**
   * Returns the angle(in degrees) between vector from this point to `p` and
   * the x-axis.
   */
  theta(p: Point.PointLike | Point.PointData = new Point()): number {
    const ref = Point.create(p);
    const y = -(ref.y - this.y); // invert the y-axis.
    const x = ref.x - this.x;
    let rad = Math.atan2(y, x);

    // Correction for III. and IV. quadrant.
    if (rad < 0) {
      rad = 2 * Math.PI + rad;
    }

    return (180 * rad) / Math.PI;
  }

  translate(dx: number, dy: number): this;
  translate(p: Point.PointLike | Point.PointData): this;
  translate(dx: number | Point.PointLike | Point.PointData, dy?: number): this {
    const t = Point.create(dx, dy);
    this.x += t.x;
    this.y += t.y;
    return this;
  }

  /**
   * Rounds the point to the given precision.
   */
  round(precision = 0) {
    this.x = GeometryUtil.round(this.x, precision);
    this.y = GeometryUtil.round(this.y, precision);
    return this;
  }

  equals(p: Point.PointLike | Point.PointData) {
    const ref = Point.create(p);
    return ref != null && ref.x === this.x && ref.y === this.y;
  }

  /**
   * Returns the distance between the point and another point `p`.
   */
  distance(p: Point.PointLike | Point.PointData) {
    return Math.sqrt(this.squaredDistance(p));
  }

  /**
   * Returns the squared distance between the point and another point `p`.
   *
   * Useful for distance comparisons in which real distance is not necessary
   * (saves one `Math.sqrt()` operation).
   */
  squaredDistance(p: Point.PointLike | Point.PointData) {
    const ref = Point.create(p);
    const dx = this.x - ref.x;
    const dy = this.y - ref.y;
    return dx * dx + dy * dy;
  }

  /**
   * If the point lies outside the rectangle `rect`, adjust the point so that
   * it becomes the nearest point on the boundary of `rect`.
   */
  adhereToRect(rect: Rectangle.RectangleLike) {
    if (!GeometryUtil.containsPoint(rect, this)) {
      this.x = Math.min(Math.max(this.x, rect.x), rect.x + rect.width);
      this.y = Math.min(Math.max(this.y, rect.y), rect.y + rect.height);
    }
    return this;
  }
}

export interface Connection {
  fromRect: string;
  toRect: string;
  fromAnchor: string;
  toAnchor: string;
  start: Point;
  end: Point;
  path: string;
}

export class Line {
  public start: Point;
  public end: Point;

  public get center() {
    return new Point((this.start.x + this.end.x) / 2, (this.start.y + this.end.y) / 2);
  }

  constructor();
  constructor(x1: number, y1: number, x2: number, y2: number);
  constructor(p1: Point.PointLike | Point.PointData, p2: Point.PointLike | Point.PointData);
  constructor(
    x1?: number | Point.PointLike | Point.PointData,
    y1?: number | Point.PointLike | Point.PointData,
    x2?: number,
    y2?: number,
  ) {
    if (typeof x1 === "number" && typeof y1 === "number") {
      this.start = new Point(x1, y1);
      this.end = new Point(x2, y2);
    } else {
      this.start = Point.create(x1);
      this.end = Point.create(y1);
    }
  }

  getCenter() {
    return this.center;
  }
}
export class Rectangle {
  public x: number;
  public y: number;
  public width: number;
  public height: number;

  public get left() {
    return this.x;
  }

  public get top() {
    return this.y;
  }

  public get right() {
    return this.x + this.width;
  }

  public get bottom() {
    return this.y + this.height;
  }

  public get origin() {
    return new Point(this.x, this.y);
  }

  public get topLeft() {
    return new Point(this.x, this.y);
  }

  public get topCenter() {
    return new Point(this.x + this.width / 2, this.y);
  }

  public get topRight() {
    return new Point(this.x + this.width, this.y);
  }

  public get center() {
    return new Point(this.x + this.width / 2, this.y + this.height / 2);
  }

  public get bottomLeft() {
    return new Point(this.x, this.y + this.height);
  }

  public get bottomCenter() {
    return new Point(this.x + this.width / 2, this.y + this.height);
  }

  public get bottomRight() {
    return new Point(this.x + this.width, this.y + this.height);
  }

  public get corner() {
    return new Point(this.x + this.width, this.y + this.height);
  }

  public get rightMiddle() {
    return new Point(this.x + this.width, this.y + this.height / 2);
  }

  public get leftMiddle() {
    return new Point(this.x, this.y + this.height / 2);
  }

  public get topLine() {
    return new Line(this.topLeft, this.topRight);
  }

  public get rightLine() {
    return new Line(this.topRight, this.bottomRight);
  }

  public get bottomLine() {
    return new Line(this.bottomLeft, this.bottomRight);
  }

  public get leftLine() {
    return new Line(this.topLeft, this.bottomLeft);
  }

  constructor(x?: number, y?: number, width?: number, height?: number) {
    this.x = x == null ? 0 : x;
    this.y = y == null ? 0 : y;
    this.width = width == null ? 0 : width;
    this.height = height == null ? 0 : height;
  }

  getOrigin() {
    return this.origin;
  }

  getTopLeft() {
    return this.topLeft;
  }

  getTopCenter() {
    return this.topCenter;
  }

  getTopRight() {
    return this.topRight;
  }

  getCenter() {
    return this.center;
  }

  getCenterX() {
    return this.x + this.width / 2;
  }

  getCenterY() {
    return this.y + this.height / 2;
  }

  getBottomLeft() {
    return this.bottomLeft;
  }

  getBottomCenter() {
    return this.bottomCenter;
  }

  getBottomRight() {
    return this.bottomRight;
  }

  getCorner() {
    return this.corner;
  }

  getRightMiddle() {
    return this.rightMiddle;
  }

  getLeftMiddle() {
    return this.leftMiddle;
  }

  getTopLine() {
    return this.topLine;
  }

  getRightLine() {
    return this.rightLine;
  }

  getBottomLine() {
    return this.bottomLine;
  }

  getLeftLine() {
    return this.leftLine;
  }

  clone() {
    return new Rectangle(this.x, this.y, this.width, this.height);
  }

  /**
   * Returns a rectangle that is a subtraction of the two rectangles if such an
   * object exists (the two rectangles intersect). Returns `null` otherwise.
   */
  intersectsWithRect(x: number, y: number, w: number, h: number): Rectangle | null;
  intersectsWithRect(rect: Rectangle.RectangleLike | Rectangle.RectangleData): Rectangle | null;
  intersectsWithRect(
    x: number | Rectangle.RectangleLike | Rectangle.RectangleData,
    y?: number,
    width?: number,
    height?: number,
  ) {
    const ref = Rectangle.create(x, y, width, height);

    // no intersection
    if (!this.isIntersectWithRect(ref)) {
      return null;
    }

    const myOrigin = this.origin;
    const myCorner = this.corner;
    const rOrigin = ref.origin;
    const rCorner = ref.corner;

    const xx = Math.max(myOrigin.x, rOrigin.x);
    const yy = Math.max(myOrigin.y, rOrigin.y);

    return new Rectangle(
      xx,
      yy,
      Math.min(myCorner.x, rCorner.x) - xx,
      Math.min(myCorner.y, rCorner.y) - yy,
    );
  }

  isIntersectWithRect(x: number, y: number, w: number, h: number): boolean;
  isIntersectWithRect(rect: Rectangle.RectangleLike | Rectangle.RectangleData): boolean;
  isIntersectWithRect(
    x: number | Rectangle.RectangleLike | Rectangle.RectangleData,
    y?: number,
    width?: number,
    height?: number,
  ) {
    const ref = Rectangle.create(x, y, width, height);
    const myOrigin = this.origin;
    const myCorner = this.corner;
    const rOrigin = ref.origin;
    const rCorner = ref.corner;

    if (
      rCorner.x <= myOrigin.x ||
      rCorner.y <= myOrigin.y ||
      rOrigin.x >= myCorner.x ||
      rOrigin.y >= myCorner.y
    ) {
      return false;
    }
    return true;
  }

  inflate(amount: number): this;
  /**
   * Returns a rectangle inflated in axis-x by `2*dx` and in axis-y by `2*dy`.
   */
  inflate(dx: number, dy: number): this;
  inflate(dx: number, dy?: number): this {
    const w = dx;
    const h = dy != null ? dy : dx;
    this.x -= w;
    this.y -= h;
    this.width += 2 * w;
    this.height += 2 * h;

    return this;
  }

  /**
   * Returns `true` if the point is inside the rectangle (inclusive).
   * Returns `false` otherwise.
   */
  containsPoint(x: number, y: number): boolean;
  containsPoint(point: Point.PointLike | Point.PointData): boolean;
  containsPoint(x: number | Point.PointLike | Point.PointData, y?: number): boolean {
    return GeometryUtil.containsPoint(this, Point.create(x, y));
  }

  /**
   * Translates the rectangle by `rect.x` and `rect.y` and expand it by
   * `rect.width` and `rect.height`.
   */
  moveAndExpand(rect: Rectangle.RectangleLike | Rectangle.RectangleData) {
    const ref = Rectangle.clone(rect);
    this.x += ref.x || 0;
    this.y += ref.y || 0;
    this.width += ref.width || 0;
    this.height += ref.height || 0;
    return this;
  }

  /**
   * Returns a rectangle that is a union of this rectangle and rectangle `rect`.
   */
  union(rect: Rectangle.RectangleLike | Rectangle.RectangleData) {
    const ref = Rectangle.clone(rect);
    const myOrigin = this.origin;
    const myCorner = this.corner;
    const rOrigin = ref.origin;
    const rCorner = ref.corner;

    const originX = Math.min(myOrigin.x, rOrigin.x);
    const originY = Math.min(myOrigin.y, rOrigin.y);
    const cornerX = Math.max(myCorner.x, rCorner.x);
    const cornerY = Math.max(myCorner.y, rCorner.y);

    return new Rectangle(originX, originY, cornerX - originX, cornerY - originY);
  }

  /**
   * Returns a point on the boundary of the rectangle nearest to the point `p`.
   */
  getNearestPointToPoint(p: Point.PointLike | Point.PointData) {
    const ref = Point.clone(p);
    if (this.containsPoint(ref)) {
      const side = this.getNearestSideToPoint(ref);
      if (side === "left") {
        return new Point(this.x, ref.y);
      }

      if (side === "top") {
        return new Point(ref.x, this.y);
      }

      if (side === "right") {
        return new Point(this.x + this.width, ref.y);
      }

      if (side === "bottom") {
        return new Point(ref.x, this.y + this.height);
      }
    }

    return ref.adhereToRect(this);
  }

  /**
   * Returns a string ("top", "left", "right" or "bottom") denoting the side of
   * the rectangle which is nearest to the point `p`.
   */
  getNearestSideToPoint(p: Point.PointLike | Point.PointData): Rectangle.Side {
    const ref = Point.clone(p);
    const distLeft = ref.x - this.x;
    const distRight = this.x + this.width - ref.x;
    const distTop = ref.y - this.y;
    const distBottom = this.y + this.height - ref.y;
    let closest = distLeft;
    let side: Rectangle.Side = "left";

    if (distRight < closest) {
      closest = distRight;
      side = "right";
    }

    if (distTop < closest) {
      closest = distTop;
      side = "top";
    }

    if (distBottom < closest) {
      side = "bottom";
    }

    return side;
  }
}

export namespace GeometryUtil {
  export function round(num: number, precision = 0) {
    return Number.isInteger(num) ? num : +num.toFixed(precision);
  }

  export function random(): number;
  export function random(max: number): number;
  export function random(min: number, max: number): number;
  export function random(min?: number, max?: number): number {
    let mmin;
    let mmax;

    if (max == null) {
      mmax = min == null ? 1 : min;
      mmin = 0;
    } else {
      mmax = max;
      mmin = min == null ? 0 : min;
    }

    if (mmax < mmin) {
      const temp = mmin;
      mmin = mmax;
      mmax = temp;
    }

    return Math.floor(Math.random() * (mmax - mmin + 1) + mmin);
  }

  export function clamp(value: number, min: number, max: number) {
    if (Number.isNaN(value)) {
      return NaN;
    }

    if (Number.isNaN(min) || Number.isNaN(max)) {
      return 0;
    }

    return min < max
      ? value < min
        ? min
        : value > max
          ? max
          : value
      : value < max
        ? max
        : value > min
          ? min
          : value;
  }

  export function snapToGrid(value: number, gridSize: number) {
    return gridSize * Math.round(value / gridSize);
  }

  export function containsPoint(rect: Rectangle.RectangleLike, point: Point.PointLike) {
    return (
      point != null &&
      rect != null &&
      point.x >= rect.x &&
      point.x <= rect.x + rect.width &&
      point.y >= rect.y &&
      point.y <= rect.y + rect.height
    );
  }

  export function squaredLength(p1: Point.PointLike, p2: Point.PointLike) {
    const dx = p1.x - p2.x;
    const dy = p1.y - p2.y;
    return dx * dx + dy * dy;
  }
}

export namespace Point {
  export function isPoint(instance: any): instance is Point {
    return instance != null && instance instanceof Point;
  }
}

export namespace Point {
  export interface PointLike {
    x: number;
    y: number;
  }

  export type PointData = [number, number];

  export type Bearing = "NE" | "E" | "SE" | "S" | "SW" | "W" | "NW" | "N";

  export function isPointLike(p: any): p is PointLike {
    return p != null && typeof p === "object" && typeof p.x === "number" && typeof p.y === "number";
  }

  export function isPointData(p: any): p is PointData {
    return (
      p != null &&
      Array.isArray(p) &&
      p.length === 2 &&
      typeof p[0] === "number" &&
      typeof p[1] === "number"
    );
  }
}

export namespace Point {
  export function create(x?: number | Point | PointLike | PointData, y?: number): Point {
    if (x == null || typeof x === "number") {
      return new Point(x, y);
    }

    return clone(x);
  }

  export function clone(p: Point | PointLike | PointData) {
    if (Point.isPoint(p)) {
      return new Point(p.x, p.y);
    }

    if (Array.isArray(p)) {
      return new Point(p[0], p[1]);
    }

    return new Point(p.x, p.y);
  }

  export function equals(p1?: Point.PointLike, p2?: Point.PointLike) {
    if (p1 === p2) {
      return true;
    }

    if (p1 != null && p2 != null) {
      return p1.x === p2.x && p1.y === p2.y;
    }

    return false;
  }

  export function equalPoints(p1: Point.PointLike[], p2: Point.PointLike[]) {
    if (
      (p1 == null && p2 != null) ||
      (p1 != null && p2 == null) ||
      (p1 != null && p2 != null && p1.length !== p2.length)
    ) {
      return false;
    }

    if (p1 != null && p2 != null) {
      for (let i = 0, ii = p1.length; i < ii; i += 1) {
        if (!equals(p1[i], p2[i])) {
          return false;
        }
      }
    }

    return true;
  }

  /**
   * Returns a point with random coordinates that fall within the range
   * `[x1, x2]` and `[y1, y2]`.
   */
  export function random(x1: number, x2: number, y1: number, y2: number) {
    return new Point(GeometryUtil.random(x1, x2), GeometryUtil.random(y1, y2));
  }

  export function rotateEx(
    point: Point | PointLike | PointData,
    cos: number,
    sin: number,
    center: Point | PointLike | PointData = new Point(),
  ) {
    const source = clone(point);
    const origin = clone(center);
    const dx = source.x - origin.x;
    const dy = source.y - origin.y;
    const x1 = dx * cos - dy * sin;
    const y1 = dy * cos + dx * sin;
    return new Point(x1 + origin.x, y1 + origin.y);
  }

  /**
   * Returns a new Point object from the given polar coordinates.
   * @see http://en.wikipedia.org/wiki/Polar_coordinate_system
   */
  export function fromPolar(
    r: number,
    rad: number,
    origin: Point | PointLike | PointData = new Point(),
  ) {
    let x = Math.abs(r * Math.cos(rad));
    let y = Math.abs(r * Math.sin(rad));
    const org = clone(origin);
    const deg = Angle.normalize(Angle.toDeg(rad));

    if (deg < 90) {
      y = -y;
    } else if (deg < 180) {
      x = -x;
      y = -y;
    } else if (deg < 270) {
      x = -x;
    }

    return new Point(org.x + x, org.y + y);
  }

  /**
   * Converts rectangular to polar coordinates.
   */
  export function toPolar(
    point: Point | PointLike | PointData,
    origin: Point | PointLike | PointData = new Point(),
  ) {
    const p = clone(point);
    const o = clone(origin);
    const dx = p.x - o.x;
    const dy = p.y - o.y;
    return new Point(
      Math.sqrt(dx * dx + dy * dy), // r
      Angle.toRad(o.theta(p)),
    );
  }
}

export namespace Rectangle {
  export function isRectangle(instance: any): instance is Rectangle {
    return instance != null && instance instanceof Rectangle;
  }
}

export namespace Rectangle {
  export type RectangleData = [number, number, number, number];

  export interface RectangleLike extends Point.PointLike {
    x: number;
    y: number;
    width: number;
    height: number;
  }

  export function isRectangleLike(o: any): o is RectangleLike {
    return (
      o != null &&
      typeof o === "object" &&
      typeof o.x === "number" &&
      typeof o.y === "number" &&
      typeof o.width === "number" &&
      typeof o.height === "number"
    );
  }

  export type Side = "left" | "right" | "top" | "bottom";

  export type KeyPoint =
    | "center"
    | "origin"
    | "corner"
    | "topLeft"
    | "topCenter"
    | "topRight"
    | "bottomLeft"
    | "bottomCenter"
    | "bottomRight"
    | "rightMiddle"
    | "leftMiddle";
}

export namespace Rectangle {
  export function create(rect: RectangleLike | RectangleData): Rectangle;
  export function create(x?: number, y?: number, width?: number, height?: number): Rectangle;
  export function create(
    x?: number | RectangleLike | RectangleData,
    y?: number,
    width?: number,
    height?: number,
  ): Rectangle;
  export function create(
    x?: number | RectangleLike | RectangleData,
    y?: number,
    width?: number,
    height?: number,
  ): Rectangle {
    if (x == null || typeof x === "number") {
      return new Rectangle(x, y, width, height);
    }

    return clone(x);
  }

  export function clone(rect: RectangleLike | RectangleData) {
    if (Rectangle.isRectangle(rect)) {
      return rect.clone();
    }

    if (Array.isArray(rect)) {
      return new Rectangle(rect[0], rect[1], rect[2], rect[3]);
    }

    return new Rectangle(rect.x, rect.y, rect.width, rect.height);
  }

  interface Size {
    width: number;
    height: number;
  }

  export function fromSize(size: Size) {
    return new Rectangle(0, 0, size.width, size.height);
  }

  export function fromPositionAndSize(pos: Point.PointLike, size: Size) {
    return new Rectangle(pos.x, pos.y, size.width, size.height);
  }
}

export namespace Angle {
  /**
   * Converts radian angle to degree angle.
   * @param rad The radians to convert.
   */
  export function toDeg(rad: number) {
    return ((180 * rad) / Math.PI) % 360;
  }

  /**
   * Converts degree angle to radian angle.
   * @param deg The degree angle to convert.
   * @param over360
   */
  export const toRad = function (deg: number, over360 = false) {
    const d = over360 ? deg : deg % 360;
    return (d * Math.PI) / 180;
  };

  /**
   * Returns the angle in degrees and clamps its value between `0` and `360`.
   */
  export function normalize(angle: number) {
    return (angle % 360) + (angle < 0 ? 360 : 0);
  }
}
